﻿using System;
using System.Collections.ObjectModel;
using System.Configuration;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.Configuration;
using JTLeigh.Commerce.Spencer.Core.Common;

namespace JTLeigh.Commerce.Spencer.Core.Service
{

	/// <summary>
	/// WCF service behavior used in conjunction with <see cref="DependencyInjectionInstanceProvider"/>
	/// to perform dependency injection on WCF services.
	/// </summary>
	/// <remarks>
	/// <para>
	/// <see cref="DependencyInjectionServiceBehaviorAttribute"/> creates a dependency injection 
	/// container, creates the dependency injection instance provider, and applies the instance 
	/// provider to all service endpoints.
	/// </para>
	/// </remarks>
	[AttributeUsage(AttributeTargets.Class)]
	public sealed class DependencyInjectionServiceBehaviorAttribute : Attribute, IServiceBehavior
	{

		#region IServiceBehavior Members

		/// <summary>
		/// Applies the dispatch behavior.
		/// </summary>
		/// <param name="serviceDescription">A <see cref="ServiceDescription"/> object describing 
		/// the service the behavior is being applied to.</param>
		/// <param name="serviceHostBase">A <see cref="ServiceHostBase"/> object representing the
		/// service host.</param>
		public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
		{
			IUnityContainer container;
			IInstanceProvider instanceProvider;
			InstanceContext singletonInstanceContext;

			Safety.AssertArgumentNotNull("serviceDescription", serviceDescription);
			Safety.AssertArgumentNotNull("serviceHostBase", serviceHostBase);

			container = CreateDependencyInjectionContainer(serviceDescription.ServiceType);
			instanceProvider = new DependencyInjectionInstanceProvider(container, serviceDescription.ServiceType);

			singletonInstanceContext = GetSingletonInstanceContext(serviceDescription, serviceHostBase);

			ConfigureEndpoints(serviceHostBase, instanceProvider, singletonInstanceContext);
		}

		/// <summary>
		/// Does nothing.
		/// </summary>
		/// <param name="serviceDescription">Ignored.</param>
		/// <param name="serviceHostBase">Ignored.</param>
		/// <param name="endpoints">Ignored.</param>
		/// <param name="bindingParameters">Ignored.</param>
		public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
		{
		}

		/// <summary>
		/// Does nothing.
		/// </summary>
		/// <param name="serviceDescription">Ignored.</param>
		/// <param name="serviceHostBase">Ignored.</param>
		public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
		{
		}

		#endregion // IServiceBehavior Members

		#region Implementation

		internal static IUnityContainer CreateDependencyInjectionContainer(Type serviceType)
		{
			IUnityContainer container;
			UnityConfigurationSection dependencyInjectionConfiguration;
			UnityContainerElement serviceSpecificContainerConfiguration;

			Safety.AssertArgumentNotNull("serviceType", serviceType);

			dependencyInjectionConfiguration = ConfigurationManager.GetSection("dependencyInjectionConfiguration") as UnityConfigurationSection;
			Safety.AssertNotNull(dependencyInjectionConfiguration, "DependencyInjectionServiceBehaviorAttribute could not load the 'dependencyInjectionConfiguration' configuration section.");

			container = new UnityContainer();
			dependencyInjectionConfiguration.Containers.Default.Configure(container);

			// NOTE: If the dependency injection configuration contains a service-specific 
			// container configuration, we create a child container and load that configuration 
			// into it, so that the service will inherit the default configuration and have the 
			// opportunity to override it.
			serviceSpecificContainerConfiguration = dependencyInjectionConfiguration.Containers[serviceType.Name];
			if (serviceSpecificContainerConfiguration != null)
			{
				container = container.CreateChildContainer();
				serviceSpecificContainerConfiguration.Configure(container);
			}

			// NOTE: We register the container as the singleton instance of IContainer so that the
			// types we resolve through the container have the ability to resolve other types 
			// without having to expose those types as dependencies.
			//
			// NOTE: If we have a child container, it's the child container that is registered, not
			// the parent container.
			container.RegisterInstance<IUnityContainer>(container);

			return container;
		}

		internal static InstanceContext GetSingletonInstanceContext(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
		{
			InstanceContext singletonInstanceContext;
			ServiceBehaviorAttribute serviceBehavior;

			Safety.AssertArgumentNotNull("serviceDescription", serviceDescription);
			Safety.AssertArgumentNotNull("serviceHostBase", serviceHostBase);

			singletonInstanceContext = null;
			serviceBehavior = serviceDescription.Behaviors.Find<ServiceBehaviorAttribute>();
			if ((serviceBehavior != null) && (serviceBehavior.InstanceContextMode == InstanceContextMode.Single))
			{
				singletonInstanceContext = new InstanceContext(serviceHostBase);
			}
			return singletonInstanceContext;
		}

		internal static void ConfigureEndpoints(ServiceHostBase serviceHostBase, IInstanceProvider instanceProvider, InstanceContext singletonInstanceContext)
		{
			Safety.AssertArgumentNotNull("serviceHostBase", serviceHostBase);
			Safety.AssertArgumentNotNull("instanceProvider", instanceProvider);
			// singletonInstanceContext can be null.

			foreach (ChannelDispatcherBase channelDispatcherBase in serviceHostBase.ChannelDispatchers)
			{
				ChannelDispatcher channelDispatcher;

				channelDispatcher = channelDispatcherBase as ChannelDispatcher;
				if (channelDispatcher != null)
				{
					foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints)
					{
						endpointDispatcher.DispatchRuntime.InstanceProvider = instanceProvider;
						if (singletonInstanceContext != null)
						{
							endpointDispatcher.DispatchRuntime.SingletonInstanceContext = singletonInstanceContext;
						}
					}
				}
			}
		}

		#endregion // Implementation

	}

}
